type Primitive = string | number | boolean | null | undefined
type PrimitiveArray = PrimitiveData[]
type PrimitiveObject = {
  [key: string]: PrimitiveData
}
type PrimitiveData = PrimitiveObject | PrimitiveArray | Primitive

export function isSameArray<T extends PrimitiveArray>(arr1?: T, arr2?: T): boolean {
  if (arr1 === arr2) {
    return true
  }
  if (!arr1 || !arr2) {
    return false
  }
  if (arr1.length !== arr2.length) {
    return false
  }
  for (let i = 0; i < arr1.length; i++) {
    if (arr1[i] !== arr2[i]) {
      return false
    }
  }
  return true
}

export function isSameObject<T extends PrimitiveObject>(a?: T, b?: T): boolean {
  if (a === b) {
    return true
  }
  if (!a || !b) {
    return false
  }
  const keysA = Object.keys(a)
  const keysB = Object.keys(b)
  if (!isSameArray(keysA, keysB)) {
    return false
  }
  return !keysA.some((key) => {
    const newItemValue = b[key as keyof T]
    const oldItemValue = a[key as keyof T]
    if (newItemValue !== oldItemValue) {
      return true
    }
    return false
  })
}

function deepReplaceObject<T extends PrimitiveObject>(a: T, b: T): T {
  if (isSameObject(a, b)) {
    return a
  }
  const c = { ...b }
  Object.keys(b).forEach((key: keyof T) => {
    c[key] = deepReplace(a[key], b[key])
  })
  if (isSameObject(a, c)) {
    return a
  }
  return c
}

function deepReplaceArray<T extends PrimitiveArray>(a: T, b: T): T {
  if (isSameArray(a, b)) {
    return a
  }
  const newB = b.map((item, index) => deepReplace(a[index], item)) as T
  if (isSameArray(a, newB)) {
    return a
  }
  return newB
}

export function deepReplace<T extends PrimitiveData>(a: T, b: T): T {
  if (a === b) {
    return a
  }
  if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
    if (Array.isArray(a) && Array.isArray(b)) {
      return deepReplaceArray(a as PrimitiveArray, b as PrimitiveArray) as T
    } else {
      return deepReplaceObject(a as PrimitiveObject, b as PrimitiveObject) as T
    }
  }
  return b
}

export const replace =
  <T extends PrimitiveData>(b: T) =>
  (a: T): T =>
    deepReplace(a, b)

export function mergeMap<Key extends string | number, Value extends PrimitiveData>(
  a: Map<Key, Value>,
  b: Map<Key, Value>
): Map<Key, Value> {
  const c = new Map<Key, Value>(a)
  b.forEach((value, key) => {
    const oldValue = c.get(key)
    if (oldValue) {
      c.set(key, deepReplace(oldValue, value))
    }
  })
  return c
}

export function mergeObject<T extends PrimitiveObject>(a: T, b: T): T {
  const c = { ...a }
  Object.keys(b).forEach((key: keyof T) => {
    c[key] = deepReplace(a[key], b[key])
  })
  return c
}

export function merge<Key extends string | number, Value extends PrimitiveData, T extends Map<Key, Value>>(b: T) {
  return (a: T): T => {
    return mergeMap(a, b) as T
  }
}

export function update<T, K extends keyof T>(data: T, key: K, updater: (v: T[K]) => T[K]) {
  const oldValue = data[key]
  const newValue = updater(oldValue)
  if (oldValue === newValue) {
    return false
  }
  data[key] = newValue
  return true
}

export function objectMap<T, U>(obj: Record<string, T>, mapper: (value: T, key: string) => U): Record<string, U> {
  const c: Record<string, U> = {}
  Object.keys(obj).forEach((key: string) => {
    c[key] = mapper(obj[key], key)
  })
  return c
}

export function push<T extends PrimitiveData, U>(addItems: T[], uniqueFn?: (v: T) => U) {
  if (uniqueFn) {
    const newItemUniqueSet = new Set<U>(addItems.map(uniqueFn))
    return (arr: T[]) => {
      if (addItems.length === 0) {
        return arr
      }
      const newArr = arr.filter((v) => !newItemUniqueSet.has(uniqueFn(v))).concat(addItems)
      return deepReplace(arr, newArr)
    }
  }
  return (arr: T[]) => {
    if (addItems.length === 0) {
      return arr
    }
    return arr.concat(addItems)
  }
}

export function toRecordObject<T extends PrimitiveData>(arr: T[], keyFn: (v: T) => string | number): Record<string, T> {
  const c: Record<string, T> = {}
  arr.forEach((v) => {
    c[keyFn(v)] = v
  })
  return c
}
